<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CosyVoice 流式测试</title>
  <style>
    body {
      font-family: 'Microsoft YaHei', sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .container {
      display: flex;
      flex-direction: column;
      gap: 15px;
    }
    textarea {
      width: 100%;
      height: 100px;
      padding: 8px;
      border-radius: 4px;
      border: 1px solid #ccc;
    }
    select, input {
      padding: 8px;
      border-radius: 4px;
      border: 1px solid #ccc;
    }
    .form-group {
      display: flex;
      flex-direction: column;
      gap: 5px;
    }
    label {
      font-weight: bold;
    }
    .controls {
      display: flex;
      gap: 10px;
      align-items: center;
    }
    button {
      padding: 10px 15px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover {
      background-color: #45a049;
    }
    #status {
      margin-top: 10px;
      font-weight: bold;
    }
    .test-buttons {
      display: flex;
      gap: 10px;
      margin-top: 10px;
    }
  </style>
</head>

<body>
  <h1>CosyVoice 流式测试</h1>
  
  <div class="container">
    <div class="form-group">
      <label for="text">文本内容:</label>
      <textarea id="text">你好，这是一个测试。今天天气很好，我们一起去公园散步吧。</textarea>
    </div>
    
    <div class="form-group">
      <label for="speaker">音色:</label>
      <select id="speaker">
        <option value="中文女">中文女</option>
        <option value="中文男">中文男</option>
        <option value="英文女">英文女</option>
        <option value="英文男">英文男</option>
        <option value="粤语女">粤语女</option>
        <option value="四川话">四川话</option>
        <option value="东北话">东北话</option>
      </select>
      <button onclick="fetchSpeakers()">获取可用音色</button>
    </div>
    
    <div class="form-group">
      <label for="speed">语速:</label>
      <input type="range" id="speed" min="0.5" max="1.5" step="0.1" value="1.0">
      <span id="speed-value">1.0</span>
    </div>
    
    <div class="form-group">
      <label for="server">服务器端口:</label>
      <input type="text" id="server" value="8001">
    </div>
    
    <div class="form-group">
      <label for="api_path">API路径:</label>
      <input type="text" id="api_path" value="/api/v1/tts/stream" style="width: 100%;">
    </div>
    
    <div class="form-group">
      <label for="sample_rate">采样率:</label>
      <select id="sample_rate">
        <option value="16000">16000 Hz (默认)</option>
        <option value="22050">22050 Hz</option>
        <option value="12000">12000 Hz (降低)</option>
        <option value="20000">20000 Hz</option>
        <option value="24000">24000 Hz (提高)</option>
      </select>
      <small style="color:#555">如果音调听起来不对，尝试调整采样率。女声音调偏低，选择更高的采样率；男声音调偏高，选择更低的采样率。</small>
    </div>
    
    <div class="controls">
      <button onclick="startStreamForm()">测试 WAV 流式输出</button>
    </div>
    
    <div class="test-buttons">
      <button onclick="testApiHealth(`http://localhost:${document.getElementById('server').value}/health`)">测试 API 可用性</button>
    </div>
    
    <div id="status">状态: 空闲</div>
  </div>

  <script>
    // 更新速度显示
    document.getElementById("speed").oninput = function() {
      document.getElementById("speed-value").textContent = this.value;
    };
    
    // 测试API健康状态
    async function testApiHealth(url) {
      const status = document.getElementById("status");
      status.textContent = `状态: 测试API连接...`;
      
      try {
        const response = await fetch(url, {
          method: 'GET'
        });
        
        if (response.ok) {
          const data = await response.json();
          status.textContent = `状态: API可用! 音色列表: ${JSON.stringify(data.speakers || '无音色信息')}`;
        } else {
          status.textContent = `状态: API错误 - ${response.status} ${response.statusText}`;
        }
      } catch (error) {
        status.textContent = `状态: 无法连接API - ${error.message}`;
      }
    }
    
    // 使用FormData方式测试流
    async function startStreamForm() {
      const text = document.getElementById("text").value;
      const speaker = document.getElementById("speaker").value;
      const speed = document.getElementById("speed").value;
      const server = document.getElementById("server").value;
      const apiPath = document.getElementById("api_path").value;
      const sampleRateSelect = document.getElementById("sample_rate").value;
      const status = document.getElementById("status");
      const url = `http://localhost:${server}${apiPath}`;
      
      console.log(`测试API: ${url}`);
      status.textContent = `状态: 请求中... ${url}`;
      
      if (!text) {
        status.textContent = "状态: 请输入文本!";
        return;
      }
      
      status.textContent = "状态: 请求中...";
      
      try {
        // 创建FormData对象
        const formData = new FormData();
        formData.append('text', text);
        formData.append('speaker_id', speaker);
        formData.append('speed', speed);
        formData.append('chunk_size', '8192');
        
        // 发送请求
        const response = await fetch(url, {
          method: 'POST',
          body: formData
        });
        
        if (!response.ok) {
          status.textContent = `状态: 请求失败 - ${response.status} ${response.statusText}`;
          return;
        }
        
        // 打印响应头信息
        console.log("响应头信息:");
        for (const [key, value] of response.headers.entries()) {
          console.log(`${key}: ${value}`);
        }
        console.log("Content-Type:", response.headers.get("Content-Type"));
        
        status.textContent = "状态: 接收音频流...";
        
        // 处理音频流
        const reader = response.body.getReader();
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const sampleRate = parseInt(sampleRateSelect); // 使用用户选择的采样率
        console.log(`使用采样率: ${sampleRate} Hz`);
        let audioBufferQueue = [];
        let source;
        let isPlaying = false;
        
        // WAV头部数据
        let hasReceivedHeader = false;
        let leftover = new Uint8Array(0);
        
        // 处理音频缓冲区
        function processBuffer() {
          if (isPlaying || !audioBufferQueue.length) return;
          console.log("处理音频缓冲区，队列长度:", audioBufferQueue.length);
          
          const tmpBufQueue = audioBufferQueue.slice();
          audioBufferQueue = [];
          const totalLength = tmpBufQueue.reduce((acc, chunk) => acc + chunk.length, 0);
          console.log("总样本数:", totalLength);
          
          // 检查长度，避免创建空音频缓冲区
          if (totalLength <= 0) {
            console.warn("音频数据长度为0，跳过处理");
            return;
          }
          
          const audioBuffer = audioContext.createBuffer(1, totalLength, sampleRate);
          const combinedArray = new Float32Array(totalLength);
          
          let offset = 0;
          while (tmpBufQueue.length) {
            const chunk = tmpBufQueue.shift();
            combinedArray.set(chunk, offset);
            offset += chunk.length;
          }
          
          audioBuffer.copyToChannel(combinedArray, 0);
          
          source = audioContext.createBufferSource();
          source.buffer = audioBuffer;
          source.connect(audioContext.destination);
          source.onended = () => {
            isPlaying = false;
            if (audioBufferQueue.length > 0) {
              processBuffer();
            }
          };
          source.start();
          isPlaying = true;
          status.textContent = "状态: 播放中...";
        }
        
        // 读取流数据
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            status.textContent = "状态: 流结束，音频处理完成";
            break;
          }
          
          // 打印收到的数据信息
          console.log(`收到数据: 长度=${value.length}字节`);
          
          // 检查是否收到空数据
          if (!value || value.length === 0) {
            console.warn("收到空数据包，跳过处理");
            continue;
          }
          
          // 处理WAV头部
          if (!hasReceivedHeader && value.length >= 44) {
            console.log("检测到WAV头部");
            hasReceivedHeader = true;
            const dataView = new DataView(value.buffer);
            // 从WAV头部读取采样率
            const wavSampleRate = dataView.getUint32(24, true);
            console.log("WAV头部采样率:", wavSampleRate);
            
            // 跳过44字节的WAV头部
            const audioData = value.slice(44);
            
            // 如果剩余的数据长度不足，则跳过处理
            if (audioData.length === 0) {
              console.log("WAV头部后没有数据，等待下一个数据块");
              continue;
            }
            
            // 合并leftover和新数据
            const combinedValue = new Uint8Array(leftover.length + audioData.length);
            combinedValue.set(leftover);
            combinedValue.set(audioData, leftover.length);
            
            // 处理16位PCM数据 (确保是2字节对齐)
            const remainder = combinedValue.length % 2;
            const validLength = combinedValue.length - remainder;
            
            if (validLength > 0) {
              const validData = combinedValue.slice(0, validLength);
              const pcmData = new Int16Array(validData.buffer);
              const float32Data = new Float32Array(pcmData.length);
              
              // 转换为[-1,1]范围的浮点数
              for (let i = 0; i < pcmData.length; i++) {
                float32Data[i] = pcmData[i] / 32767;
              }
              
              if (float32Data.length > 0) {
                audioBufferQueue.push(float32Data);
                console.log(`添加到缓冲队列: ${float32Data.length}样本`);
              }
              
              // 保存剩余数据
              leftover = combinedValue.slice(validLength);
            } else {
              // 数据不足2字节，保存到下一次
              leftover = combinedValue;
            }
          } 
          else if (hasReceivedHeader) {
            // 合并leftover和新数据
            const combinedValue = new Uint8Array(leftover.length + value.length);
            combinedValue.set(leftover);
            combinedValue.set(value, leftover.length);
            
            // 确保16位PCM数据对齐
            const remainder = combinedValue.length % 2;
            const validLength = combinedValue.length - remainder;
            
            if (validLength > 0) {
              // 处理有效数据
              const validData = combinedValue.slice(0, validLength);
              const pcmData = new Int16Array(validData.buffer);
              const float32Data = new Float32Array(pcmData.length);
              
              // 转换为[-1,1]范围的浮点数
              for (let i = 0; i < pcmData.length; i++) {
                float32Data[i] = pcmData[i] / 32767;
              }
              
              if (float32Data.length > 0) {
                audioBufferQueue.push(float32Data);
                console.log(`添加到缓冲队列: ${float32Data.length}样本`);
              }
              
              // 保存剩余数据
              leftover = combinedValue.slice(validLength);
            } else {
              // 数据不足2字节，保存到下一次
              leftover = combinedValue;
            }
          } 
          else {
            // 还没有收到完整头部，累积数据
            const newLeftover = new Uint8Array(leftover.length + value.length);
            newLeftover.set(leftover);
            newLeftover.set(value, leftover.length);
            leftover = newLeftover;
            console.log(`累积数据等待WAV头部: ${leftover.length}字节`);
          }
          
          // 处理缓冲区
          processBuffer();
        }
      } catch (error) {
        console.error("流处理错误:", error);
        status.textContent = `状态: 错误 - ${error.message}`;
      }
    }
    
    // 获取可用音色
    async function fetchSpeakers() {
      const server = document.getElementById("server").value;
      const status = document.getElementById("status");
      const url = `http://localhost:${server}/api/v1/list_speakers`;
      
      status.textContent = `状态: 获取音色列表...`;
      
      try {
        const response = await fetch(url, {
          method: 'GET'
        });
        
        if (response.ok) {
          const data = await response.json();
          
          if (data.speakers && Array.isArray(data.speakers)) {
            const speakerSelect = document.getElementById("speaker");
            
            // 保存当前选择的值
            const currentValue = speakerSelect.value;
            
            // 清空现有选项
            speakerSelect.innerHTML = '';
            
            // 添加新的选项
            data.speakers.forEach(speaker => {
              const option = document.createElement("option");
              option.value = speaker;
              option.textContent = speaker;
              speakerSelect.appendChild(option);
            });
            
            // 尝试恢复之前的选择
            try {
              speakerSelect.value = currentValue;
            } catch (e) {
              // 如果之前的选择不再可用，使用第一个选项
              if (data.speakers.length > 0) {
                speakerSelect.value = data.speakers[0];
              }
            }
            
            status.textContent = `状态: 已获取 ${data.speakers.length} 个音色`;
          } else {
            status.textContent = `状态: API没有返回有效的音色列表`;
          }
        } else {
          status.textContent = `状态: 获取音色列表失败 - ${response.status} ${response.statusText}`;
        }
      } catch (error) {
        status.textContent = `状态: 获取音色列表错误 - ${error.message}`;
      }
    }
  </script>
</body>

</html> 